package dp.mobile.glwrapper;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.HashMap;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11Ext;

import dp.mobile.entity.generator.GeneratorFactory;
import dp.mobile.game.Game;
import dp.mobile.system.Utilities;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.opengl.GLSurfaceView;
import android.util.Log;

public class GameRenderer implements GLSurfaceView.Renderer {
	private static final String TAG = "Game Renderer";
	
	protected GameRenderer() {
		mHeight 	= Utilities.SCREEN_HEIGHT;
		mWidth 		= Utilities.SCREEN_WIDTH;
		mScale 		= new PointF(1, 1);
		mDepth		= Utilities.MAX_DEPTH;
		mOffset		= new Point(0, 0);
		mVBOMap		= new HashMap<Point, Integer>();
		mVBOSeeker	= new Point();
		mBuffer		= new int[1];
		
		mVerBuf 	= ByteBuffer.allocateDirect(4 * 3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
		mTexBuf 	= ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
		mIndBuf 	= ByteBuffer.allocateDirect(6 * 2).order(ByteOrder.nativeOrder()).asShortBuffer();
		
		mIndBuf.put(QUAD_INDICES);
		mIndBuf.position(0);
		
		mTextureLibrary = new GLTextureLibrary();
		
		mLastFrameTime 	= System.currentTimeMillis();
		mFrame 			= 0;
	}
	
	protected GameRenderer(Context context, Game game) {
		this();
		mContext 	= context;
		mGame 		= game;
	}
	
	// draw using standard quad
	public void draw(GL10 gl, GLTexture texture, int x, int y, int width, int height, float depth) {
		draw(gl, texture, DEFAULT_TEXTURE, x, y, width, height, depth, 0);
	}
	
	// draw using tiled texture
	public void draw(GL10 gl, GLTexture texture, final float[] tCoords, int x, int y, int width, int height, float depth) {
		draw(gl, texture, tCoords, x, y, width, height, depth, 0);
	}
	
	// draw using standard quad with rotation
	public void draw(GL10 gl, GLTexture texture, int x, int y, int width, int height, float depth, float rotation) {
		draw(gl, texture, DEFAULT_TEXTURE, x, y, width, height, depth, rotation);
	}
	
	// draw using standard quad with tiled texture
	public void draw(GL10 gl, GLTexture texture, final float[] tCoords, int x, int y, int width, int height, float depth, float rotation) {
		mVBOSeeker.set(width, height);
		Integer vertexPointer = mVBOMap.get(mVBOSeeker);

		final float fWidth	= width * mScale.x;
		final float fHeight	= height * mScale.y;
		final float left 	= (x + mOffset.x ) * mScale.x;
		final float top		= (- (y + height) + mOffset.y) * mScale.y;
		final float fDepth	= mDepth + depth + 0.001f * top / (float)Utilities.SCREEN_HEIGHT;
		GL11 gl11 = (GL11)gl;
		
		if (vertexPointer == null) {
			final float[] vCoords = {
				0, 		0, 			0,
				fWidth,	0, 			0,
				0,		fHeight, 	0,
				fWidth,	fHeight,	0
			};
			
			mVerBuf.put(vCoords);
			mVerBuf.position(0);
			
			//prepare VBO
			gl11.glGenBuffers(1, mBuffer, 0);
			vertexPointer = Integer.valueOf(mBuffer[0]);
			gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexPointer);
	        gl11.glBufferData(GL11.GL_ARRAY_BUFFER, 48, mVerBuf, GL11.GL_STATIC_DRAW);
	        
	        mVBOMap.put(new Point(mVBOSeeker), vertexPointer);
		}
		
		gl.glBindTexture(GL10.GL_TEXTURE_2D, texture.Name);
		
		if (cullTexture(left, top, fWidth, fHeight)) return;
		
		/*final float right	= left + fWidth;
		final float bottom	= top + fHeight;
		final float[] vCoords = {
			left,	top, fDepth,
			right, 	top, fDepth,
			left,	bottom, fDepth,
			right, 	bottom, fDepth,
		};*/
		
		//mVerBuf.put(vCoords);
		mTexBuf.put(tCoords);
		
		//mVerBuf.position(0);
		mTexBuf.position(0);
		
		//gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVerBuf);
		gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexPointer);
        gl11.glVertexPointer(3, GL10.GL_FLOAT, 0, 0);
        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mTexBuf);

		gl.glMatrixMode(GL10.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		if (rotation != 0) {
			final float centerX = (x + mOffset.x + width / 2) * mScale.x;
			final float centerY = (-y  + mOffset.y - height / 2) * mScale.y;
			gl.glTranslatef(centerX, centerY, 0);
			gl.glRotatef(rotation, 0, 0, 1);
			gl.glTranslatef(-centerX, -centerY, 0);
		}
		
		gl.glTranslatef(left, top, fDepth);
		
		// set tint
		//final float alpha 	= (float)((((texture.Tint >> 24) % 256) + 256) % 256) / 255.0f;
		final float red		= (float)((((texture.Tint >> 16L) % 256) + 256) % 256) / 255.0f;
		final float green	= (float)((((texture.Tint >> 8L) % 256) + 256) % 256) / 255.0f;
		final float blue	= (float)(((texture.Tint % 256) + 256) % 256) / 255.0f;
		gl.glColor4f(red, green, blue, 1.0f);

        gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
		gl.glDrawElements(GL10.GL_TRIANGLES, QUAD_INDICES.length, GL10.GL_UNSIGNED_SHORT, mIndBuf);
		gl.glPopMatrix();
	}
	
	//draw text
	public void drawText(GL10 gl, String text, float x, float y, float scale, int tint) {
		mFontLibrary.drawText(gl, text, x, y, scale, tint);
	}
	
	//draw text without tint
	public void drawText(GL10 gl, String text, float x, float y, float scale) {
		mFontLibrary.drawText(gl, text, x, y, scale, 0xffffffff);
	}
	
	public void onDrawFrame(GL10 gl) {
		if (!mTextureLibrary.isValidated())
			mTextureLibrary.loadAll(mContext, gl);
		++mFrame;
		if (System.currentTimeMillis() - mLastFrameTime > 1000) {
			Log.d(TAG, "FPS " + mFrame);
			mFrame = 0;
			mLastFrameTime = System.currentTimeMillis();
		}
		
    	gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
		
        //do drawing here
        synchronized (this) {
			mGame.draw(gl, mWidth, mHeight, mScale.x, mScale.y);
		}
        // end draw
	}
	
	public boolean cullTexture(float x, float y, float width, float height) {
        final float viewWidth	= mWidth * -mDepth 	/ 2.0f;
        final float viewHeight	= mHeight * -mDepth / 2.0f;
        final float posX = x;
        final float posY = y;
        
        if (posX + width < -viewWidth
    		|| posX > viewWidth 
            || posY + height < -viewHeight
            || posY > viewHeight) {
            return true;
        }
        
        return false;
	}
	
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		mScale.x =	(float)width / Utilities.SCREEN_WIDTH;
		mScale.y =	(float)height / Utilities.SCREEN_HEIGHT;

		mWidth 	= (int)(Utilities.SCREEN_WIDTH	* mScale.x);
		mHeight	= (int)(Utilities.SCREEN_HEIGHT	* mScale.y);
		
		// change frustum and viewport
		gl.glMatrixMode(GL10.GL_PROJECTION);
		float size = mWidth / 2;
		float ratio = (float)mWidth / (float)mHeight;
		gl.glFrustumf(-size, size, -size / ratio, size / ratio, 1, 10f);
		gl.glViewport(0, 0, mWidth, mHeight);
	}
	
	public void onSurfaceCreated(GL10 gl, EGLConfig glConfig) {
		Log.d(TAG, "Surface Created");
		
		// initialize OpenGL to use fastest and most efficient drawing mode
		gl.glMatrixMode(GL10.GL_MODELVIEW);
		gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
        //gl.glClearColor(0.3f, 0.6f, 0.2f, 1.0f);
        gl.glShadeModel(GL10.GL_FLAT);
        
        //enabling state
        gl.glEnable(GL10.GL_TEXTURE_2D);
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glEnable(GL10.GL_BLEND);
        gl.glEnable(GL10.GL_ALPHA_TEST);
        gl.glDisable(GL10.GL_DITHER);
        gl.glDisable(GL10.GL_LIGHTING);
        
        // drawing stuffs
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
        gl.glAlphaFunc(GL10.GL_GREATER, 0.1f);
        gl.glTexEnvx(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        
        // invalidate texture, used when paused
        mTextureLibrary.invalidateAll();
        loadTextures(gl);
        loadGLFontLibrary(gl, Utilities.MAIN_FONT);
	}
	
	public void bindTexture(GL10 gl, GLTexture texture) {
        gl.glBindTexture(GL10.GL_TEXTURE_2D, texture.Name);
	}
	
	public void loadTextures(GL10 gl) {
        if (gl != null) {
            mTextureLibrary.loadAll(mContext, gl);
        }
    }
	
	public GLTexture getRangeImage() {
		return mTextureLibrary.getRangeImage();
	}
	
	public GLTexture getHealthBarImage() {
		return mTextureLibrary.getHealthBarImage();
	}
	
	public GLTexture getHealthBarImage(int index) {
		return mTextureLibrary.getHealthBarImage(index);
	}
	
	public GLTexture getProgressBarImage() {
		return mTextureLibrary.getProgressBarImage();
	}
	
	public void loadGLFontLibrary(GL10 gl, String fontPath) {
		try {
			mFontLibrary = GLFontLibrary.createFont(gl, fontPath);
		} catch (IOException iEx) {}
	}
    
    public void flushTextures(GL10 gl) {
        if (gl != null) {
        	mTextureLibrary.deleteAll(gl);
        }
    }
    
    public void invalidateAllTextures() {
    	mTextureLibrary.invalidateAll();
    }
    
    public void invalidateVBO() {
    	mVBOMap.clear();
    }
    
    public void fakeInvalidate() {
    	mTextureLibrary.fakeInvalidate();
    }
    
    public GLTexture allocateTexture(String filePath) {
    	return mTextureLibrary.allocateTexture(filePath, false);
    }
    
    public GLTexture allocateTexture(String filePath, boolean needBound) {
    	return mTextureLibrary.allocateTexture(filePath, needBound);
    }
    
    public void setContext(Context context) {
		mContext = context;
	}
	
	public void setGame(Game game) {
		mGame = game;
	}
    
    public int getHeight() {
    	return mHeight;
    }
    
    public int getWidth() {
    	return mWidth;
    }
    
    public void setDepth(float depth) {
    	if (depth > Utilities.MAX_DEPTH)
    		depth = Utilities.MAX_DEPTH;
    	else if (depth < Utilities.MIN_DEPTH)
    		depth = Utilities.MIN_DEPTH;
    	
    	mDepth = depth;
    }
    
    public float getDepth() {
    	return mDepth;
    }
    
    public Point getOffset() {
    	return mOffset;
    }
    
    public void checkOffset(int rightMap, int bottomMap) {
    	final float right 	= -Utilities.SCREEN_WIDTH / 2;
    	final float left 	= - rightMap - right;
    	final float top 	= Utilities.SCREEN_HEIGHT / 2;
    	final float bottom 	= bottomMap - top;
    	
    	if (mOffset.x < left)
    		mOffset.x = (int)left;
    	if (mOffset.x > right)
    		mOffset.x = (int)right;
    	if (mOffset.y < top)
    		mOffset.y = (int)top;
    	if (mOffset.y > bottom)
    		mOffset.y = (int)bottom;
    }
    
    public PointF getScale() {
    	return mScale;
    }
    
    public static GameRenderer instance(Context context, Game game) {
    	if (sInstance == null)
    		sInstance = new GameRenderer();
    	sInstance.setContext(context);
    	sInstance.setGame(game);
    	return sInstance;
    }
    
    public static GameRenderer instance() {
    	if (sInstance == null)
    		sInstance = new GameRenderer();
    	
    	return sInstance;
    }
    
    public synchronized void waitDrawingComplete() {
    }
    	
	// camera stuff 
	private int mWidth;
	private int mHeight;
	private Point mOffset;
	private PointF mScale;
	private float mDepth;
	
	// system
	private Context mContext;
	private GLTextureLibrary mTextureLibrary;
	private GLFontLibrary mFontLibrary;
	
	// game stuff
	private Game mGame;
	
	// openGL stuff
	private FloatBuffer mVerBuf;
	private FloatBuffer mTexBuf;
	private ShortBuffer mIndBuf;
	
	// singleton
	private static GameRenderer sInstance;
	private static final short[] QUAD_INDICES = {0, 1, 2, 1, 2, 3};
	public static final float[] DEFAULT_TEXTURE = {
			0, 0,
			1, 0,
			0, 1,
			1, 1
		};
	
	public static final float[] FLIPPED_TEXTURE = {
		0, 1,
		1, 1,
		0, 0,
		1, 0
	};
	
	private int[] mBuffer;
	private Point mVBOSeeker;
	private HashMap<Point, Integer> mVBOMap;
	private int mFrame;
	private long mLastFrameTime;
}
